#pragma once

#include <iostream>
#include <string>         //std::string
#include <unistd.h>       //fork()/close()
#include <sys/types.h>    //open()/waitpid()
#include <sys/stat.h>     //open()
#include <fcntl.h>        //open()
#include <sys/wait.h>     //waitpid()
#include <sys/time.h>     //setrlimit()
#include <sys/resource.h> //setrlimit()

#include "../common/util.hpp" //引入comm目录下的工具模块
#include "../common/log.hpp"  //引入comm目录下的工日志模块

//运行模块
namespace ns_runner
{
    using namespace ns_util;//展开工具模块
    using namespace ns_log;//展开日志模块

    class Runner
    {
    public:
        Runner() {}
        ~Runner() {}

    public:
        /********************************************************
         * 设置进程占用资源大小
         *  该函数不仅仅是为了限制用户代码的运行时间和占用空间;
         *  其更大的意义在于防止恶意用户恶意占用服务器资源。
         ********************************************************/
        static void SetProcLimit(int _cpu_limit, int _mem_limit)
        {
            /**************************************************************
             * setrlimit(限制的资源类型, 限制结构体):用于设置进程的资源限制
             * 第一个参数 : 设置限制的资源类型。
             *     RLIMIT_CPU   : 设置CPU时长(用于设置程序运行时间)
             *     RLIMIT_AS    : 设置堆空间大小(用于设置程序能开辟的堆空间上限)
             *     RLIMIT_STACK : 设置栈空间大小。
             * 第二个参数 : 限制结构体，保存了限制的相关数值。
             *     rlim_max : 硬限制，软限制修改的上限。
             *     rlim_cur : 软限制，进程占用资源的上限。
             * 资源不足，导致OS中止进程，是通过信号终止的。
             *     经测试堆空间不足，由6号信号终止
             *     经测试时间超限，由24号信号终止
             **************************************************************/
            // 设置CPU时长（s）
            struct rlimit cpu_rlimit;
            cpu_rlimit.rlim_max = RLIM_INFINITY; //硬限制，软限制修改的上限
            cpu_rlimit.rlim_cur = _cpu_limit;    //软限制，进程占用资源的上限
            setrlimit(RLIMIT_CPU, &cpu_rlimit);

            // 设置堆空间大小 (byte->KB)
            struct rlimit mem_rlimit;
            mem_rlimit.rlim_max = RLIM_INFINITY;     //RLIM_INFINITY：设置为无穷不约束
            mem_rlimit.rlim_cur = _mem_limit * 1024; //转化成为KB
            setrlimit(RLIMIT_AS, &mem_rlimit);
        }
        
        /************************************************************************
         * 运行函数：实现编译模块创建的可执行程序的在限制的时间和空间下运行。
         *     通过创建子进程，子进程调用程序替换函数来完成可执行程序的运行。
         *     运行成功与否，父进程通过接收子进程返回信号来判断。
         * 输入参数：
         *     file_name  : 文件名，我们能根据该文件名拼接出要运行的文件的文件名。
         *     time_limit : 该程序运行的时候，可以使用的最大运行时间上限(s)。
         *     mem_limit  : 该程序运行的时候，可以申请的堆空间上限(KB)。
         * 返回值：
         *     返回值 > 0 : 程序异常，退出时收到了信号，返回值的低7位就是对应的信号。
         *     返回值 == 0: 正常运行完毕的，结果保存到了对应的临时文件中。
         *     返回值 < 0 : 内部错误。
         ************************************************************************/
        static int Run(const std::string &file_name, int time_limit, int mem_limit)
        {
            /*********************************************
             * 程序运行的两种情况：
             *     1.代码能跑完，程序能正常运行
             *     2.代码没跑完，程序异常了，不能正常运行
             * 运行模块不是判题模块，这里只考虑程序能否正常运行完毕。
             * 结果正确与否：由判题模块调用测试用例决定。
             *
             * 一个程序在启动的时候会默认打开三个文件：
             *     标准输入: 程序从这里获取输入
             *     标准输出: 保存程序运行完成时输出结果
             *     标准错误: 保存程序运行时的错误信息
             *********************************************/           
            //1.创建子进程
            pid_t pid = fork();
            if (pid < 0) //创建子进程失败
            {
                LOG(ERROR) << "运行时创建子进程失败，运行模块无法再向下执行。" << "\n";
                return -2; //返回错误码-2，代表创建子进程失败
            }
            else if (pid == 0) //创建子进程成功，打开和创建标准文件，完成标准文件的重定向 和 程序替换运行可执行程序
            {
            //2.根据拼接好的文件名打开和创建标准文件
                std::string _execute = PathUtil::Exe(file_name);    //可执行程序的文件名
                std::string _stdin   = PathUtil::Stdin(file_name);  //标准输入文件的文件名
                std::string _stdout  = PathUtil::Stdout(file_name); //标准输出文件的文件名
                std::string _stderr  = PathUtil::Stderr(file_name); //标准错误文件的文件名
                umask(0); //初始化系统掩码
                int _stdin_fd  = open(_stdin.c_str(), O_CREAT|O_RDONLY, 0644);  //根据文件名以只读方式打开标准输入文件
                int _stdout_fd = open(_stdout.c_str(), O_CREAT|O_WRONLY, 0644); //根据文件名创建并以只写方式打开标准输出文件
                int _stderr_fd = open(_stderr.c_str(), O_CREAT|O_WRONLY, 0644); //根据文件名创建并以只写方式打开标准错误文件
                if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0) //打开文件可能失败
                {
                    LOG(ERROR) << "运行时打开标准文件失败，运行模块无法再向下执行。" << "\n";
                    return -2; //返回错误码-2，代表打开文件失败
                } 
            //3.完成标准文件的重定向
                dup2(_stdin_fd, 0);  //输入重定向，程序的输入到指定内容中获取
                dup2(_stdout_fd, 1); //输出重定向，程序的结果输出到指定内容中
                dup2(_stderr_fd, 2); //错误重定向，程序执行时的错误输出到指定内容中

            //5.限制程序运行的时间和空间
                SetProcLimit(time_limit, mem_limit);
                
            //4.子进程执行程序替换运行可执行程序
                execl(_execute.c_str()/*要执行谁*/, _execute.c_str()/*执行该程序的选项argv*/, nullptr);
                
                LOG(ERROR) << "运行可执行程序失败，运行模块无法再向下执行。" << "\n";
                close(_stdin_fd);  //关闭文件
                close(_stdout_fd);
                close(_stderr_fd);
                exit(1);
            }
            else
            {
            //5.父进程接收子进程返回信号并返回，由上层来判断程序执行是否异常
                /********************************************************************
                 * waitpid(子进程pid, 输出型参数子进程退出码，是否阻塞等待)
                 *     第二个参数：用来接收子进程的退出码，异常退出退出码的低7位即为对应信号。
                 *     第三个参数：父进程是否阻塞等待子进程退出。
                 ********************************************************************/
                int status = 0;
                waitpid(pid, &status, 0);
                // 如果程序运行异常，一定是因为收到了信号！程序的退出码就是为对应信号。
                LOG(INFO) << "运行完毕, 程序返回信号: " << (status & 0x7F) << "\n"; 
                return status & 0x7F; //status & 0x7F，status的低7位才表示错误信号
            }
        }
    };
}